 #!/home/wcaarls/compile/bin/python

import arch, simulate, jobfarm, sys, thread, time, copy, os, traceback, random, re

def bound(v, l, u):
    if v < l:
      return l
    elif v > u:
      return u
    else:
      return v

class FList(list):
    def __init__(self, l=[]):
      list.__init__(self, l)
    
    def find(self, o):
      for i in range(len(self)):
        if self[i] == o:
          return i

      return -1

    def findclosest(self, o):
      best = self[0]
      for v in self:
        if abs(o-v) < abs(o-best):
          best = v
      return best

class Hashable:
    def __eq__(self, other):
        try:
            return self.__repr__() == other.__repr__()
        except:
            return False
        
    def __hash__(self):
        return self.__repr__().__hash__()

    def __ne__(self, other):
        return not (self == other)

class SystemDesc(Hashable):
    def __init__(self, archdesc, mapdesc):
      self.archdesc = archdesc
      self.mapdesc = mapdesc

    def __repr__(self):
        return "{" + str(self.archdesc) + ", " + str(self.mapdesc) + "}"

    def Generate(self):
      return arch.System(self.archdesc.Generate(), self.mapdesc.Generate())

    def dof(self):
      return self.archdesc.dof() + self.mapdesc.dof()

    def vary(self):
      variations = []
      for av in self.archdesc.vary():
        variations.append(SystemDesc(av, copy.deepcopy(self.mapdesc)))
      for mv in self.mapdesc.vary():
        variations.append(SystemDesc(copy.deepcopy(self.archdesc), mv))
      return variations
      
    def vary_random(self, p=1.0, sigma=0.5):
      return SystemDesc(self.archdesc.vary_random(p/2.0/8.0, sigma), self.mapdesc.vary_random(p/2.0/4.0, sigma))

    def evaluate(self, results, program="robocup", elaborate=0, noise=0.0, nr=1, postscript=None, lxt=None):
      for i in range(nr):
        dosim(program, self, results, elaborate, noise, postscript, lxt)

    def evalquick(self):
      cost = simulate.cost(self.Generate())
      return (None, cost, None)

    def OutputMatlab(self, results, seq=[1]):
      for i in range(len(results)):
        print "A{%d, 4, %d} = '%s';" % (seq[0], i+1, str(self.Generate()))
        print "A{%d, 1, %d} = %f;" % (seq[0], i+1, results[i][0])
        print "A{%d, 2, %d} = %f;" % (seq[0], i+1, results[i][1])
        print "A{%d, 3, %d} = %f;" % (seq[0], i+1, results[i][2])
        
      seq[0] = seq[0] + 1
      sys.stdout.flush()
      
      return seq[0] - 1

class MapDesc(Hashable):
    def __init__(self, maxtime, avgtime, avgsqtime, avgenergy, avgresources, resolution=0.25):
      self.maxtime = maxtime   
      self.avgtime = avgtime
      self.avgsqtime = avgsqtime
      self.avgenergy = avgenergy
      self.avgresources = avgresources
      self.resolution = resolution
      self.normalize()
      
    def __repr__(self):
      return "{%f, %f, %f, %f, %f}" % (self.maxtime, self.avgtime, self.avgsqtime, self.avgenergy, self.avgresources)

    def Generate(self):
      return arch.Mapper("combinatorial", "4", self.maxtime, self.avgtime, self.avgsqtime, self.avgenergy, self.avgresources)

    def dof(self):
      return 10
      
    def vary_one(self, d):
      new = copy.deepcopy(self)
      if d == 0:
        new.maxtime = max(0, self.maxtime - self.resolution)
      elif d == 1:
        new.maxtime = min(1, self.maxtime + self.resolution)
      elif d == 2:
        new.avgtime = max(0, self.avgtime - self.resolution)
      elif d == 3:
        new.avgtime = min(1, self.avgtime + self.resolution)
      elif d == 4:
        new.avgsqtime = max(0, self.avgsqtime - self.resolution)
      elif d == 5:
        new.avgsqtime = min(1, self.avgsqtime + self.resolution)
      elif d == 6:
        new.avgenergy = max(0, self.avgenergy - self.resolution)
      elif d == 7:
        new.avgenergy = min(1, self.avgenergy + self.resolution)
      elif d == 8:
        new.avgresources = max(0, self.avgresources - self.resolution)
      elif d == 9:
        new.avgresources = min(1, self.avgresources + self.resolution)
      new.normalize()
      return new
      
    def vary(self):
      variations = []
      for i in range(self.dof()):
        variations.append(self.vary_one(i))
      return variations

    def vary_random(self, p=0.125, sigma=0.5):
      new = copy.deepcopy(self)
      for d in range(4):
        if random.random() < p:
          r = random.gauss(1, sigma)
          
          if d == 0:
            new.maxtime = bound(self.maxtime*r, 0.0, 1.0)
          if d == 1:
            new.avgtime = bound(self.avgtime*r, 0.0, 1.0)
          if d == 2:
            new.avgsqtime = bound(self.avgsqtime*r, 0.0, 1.0)
          if d == 3:
            new.avgenergy = bound(self.avgenergy*r, 0.0, 1.0)
      new.normalize()
      return new

    def normalize(self):
      self.maxtime = int(self.maxtime/self.resolution)*self.resolution
      self.avgtime = int(self.avgtime/self.resolution)*self.resolution
      self.avgsqtime = int(self.avgsqtime/self.resolution)*self.resolution
      self.avgenergy = int(self.avgenergy/self.resolution)*self.resolution
      self.avgresources = int(self.avgresources/self.resolution)*self.resolution

class ComponentDesc:
    def __init__(self, type, frequency, memory, bandwidth):
      self.type = type
      self.frequency = frequency
      if float(self.frequency) == 0:
        self.frequency = 1.0
      self.memory = memory
      self.bandwidth = bandwidth

    def __repr__(self):
      return "{" + self.type + ", " + str(self.frequency) + ", " + str(self.memory) + ", " + str(self.bandwidth) + "}"

class ArchDesc:
    def __init__(self, type, bandwidth, components, static=0, alltypes=[], allbandwidths=[], allcomponents=[], allcomponentbws=[], allfrequencies=[]):
      self.type = type
      self.bandwidth = bandwidth
      self.components = components
      self.static = static

      self.alltypes = FList(alltypes)
      self.allbandwidths = FList(allbandwidths)
      self.allcomponents = FList(allcomponents)
      self.allcomponentbws = FList(allcomponentbws)
      self.allfrequencies = FList(allfrequencies)
      
      self.normalize()

    def Generate(self):
      a = arch.Architecture(self.type, self.bandwidth)
      
      for c in self.components:
        a.AddComponent(arch.Component(c.type, c.frequency, c.memory, c.bandwidth))

      return a        
    def dof(self):
      return 10
    def vary_one(self, d):
      new = copy.deepcopy(self)
      
      if d == 0:
        t = self.alltypes.find(self.type)
        new.type = self.alltypes[bound(t-1, 0, len(self.alltypes)-1)]
      elif d == 1:
        t = self.allbandwidths.find(self.bandwidth)
        new.bandwidth = self.allbandwidths[bound(t-1, 0, len(self.allbandwidths)-1)]
      elif d == 2:
        if len(self.components) > self.static:
          d = random.randrange(self.static, len(self.components))
          new.components = []
          for i in range(len(self.components)):
            if i != d:
              new.components.append(self.components[i])
      elif d == 3:
        r = random.randrange(0, len(self.components))
        t = self.allcomponentbws.find(self.components[r].bandwidth)
        new.components[r].bandwidth = self.allcomponentbws[bound(t-1, 0, len(self.allbandwidths)-1)]

      elif d == 4:
        t = self.alltypes.find(self.type)
        new.type = self.alltypes[bound(t+1, 0, len(self.alltypes)-1)]
      elif d == 5:
        t = self.allbandwidths.find(self.bandwidth)
        new.bandwidth = self.allbandwidths[bound(t+1, 0, len(self.allbandwidths)-1)]
      elif d == 6:
        t = self.allcomponents[random.randrange(0, len(self.allcomponents))]
        b = self.allcomponentbws[random.randrange(0, len(self.allcomponentbws))]
        new.components.append(ComponentDesc(t, 0, 0, b))
      elif d == 7:
        r = random.randrange(0, len(self.components))
        t = self.allcomponentbws.find(self.components[r].bandwidth)
        new.components[r].bandwidth = self.allcomponentbws[bound(t+1, 0, len(self.allcomponentbws)-1)]
      elif d == 8:
        r = random.randrange(0, len(self.components))
        new.components[r].frequency = max(new.components[r].frequency-0.1, 0.5)
      elif d == 9:
        r = random.randrange(0, len(self.components))
        new.components[r].frequency = min(new.components[r].frequency+0.1, 1.5)

      new.normalize()    
      return new

    def vary(self):
      variations = []
      for i in range(self.dof()):
        variations.append(self.vary_one(i))
      return variations
      
    def vary_random(self, p=0.0625, sigma=0.5):
      new = copy.deepcopy(self)
      for d in range(2):
        if random.random() < p:
          r = random.gauss(1, sigma)
          
          if d == 0:
            # Change network type 
            t = self.alltypes.find(self.type)
            new.type = self.alltypes[int(round(bound(t*r, 0, len(self.alltypes)-1)))]
          if d == 1:
            # Change network bandwidth

            # ####################################
            # # ATTENTION! BANDWIDTH RESOLUTION! #
            # ####################################

            new.bandwidth = self.allbandwidths.findclosest(self.bandwidth*r*10)
      
      for d in range(len(new.components)):
        # Change component frequency
        if random.random() < p:
          r = random.gauss(1, sigma)
          new.components[d].frequency = self.allfrequencies.findclosest(new.components[d].frequency*r)
        
      while random.random() < p:
        # Add component
        t = self.allcomponents[random.randrange(0, len(self.allcomponents))]
        b = self.allcomponentbws[random.randrange(0, len(self.allcomponentbws))]
        f = self.allfrequencies[random.randrange(0, len(self.allfrequencies))]
        new.components.append(ComponentDesc(t, f, 0, b))
        
      while random.random() < p:
        # Remove component
        if len(new.components) > self.static:
          d = random.randrange(self.static, len(new.components))
          newcomponents = []
          for i in range(len(new.components)):
            if i != d:
              newcomponents.append(new.components[i])
          new.components = newcomponents
        
      new.normalize()
      return new
    
    def __repr__(self):
        r = "{" + self.type + ", " + str(self.bandwidth) + ", {"
        for c in self.components:
          r += str(c) + ", "
        r += "}}"
        
        return r

    def __eq__(self, other):
        try:
            return self.__repr__() == other.__repr__()
        except:
            return False
        
    def __hash__(self):
        return self.__repr__().__hash__()

    def __ne__(self, other):
        return not (self == other)      

    def normalize(self):
      foundilp = False
    
      # Make sure we always keep an ILP
      for c in self.components:
        c.bandwidth = self.bandwidth
        if c.type == "tm" or c.type == "ss" or c.type == "m24kc":
          foundilp = True
      if not foundilp:
        self.components.append(ComponentDesc(["tm", "m24kc"][random.randrange(0, 2)], 0, 0, self.bandwidth))

def _dosim(program, architecture, address, results, elaborate=0, noise=0.0, postscript=None, lxt=None):
    try:
        sys.stdout.flush()
        (res, energy) = simulate.simulate(program, architecture.Generate(), address, elaborate, postscript, lxt, noise)
        cost = simulate.cost(architecture.Generate())
        
        # Avoid absurd architectures
        if cost > 1000:
          if elaborate:
            res = (99999, [])
          else:
            res = 99999
          energy = 99999
          cost = 99999
        
        results.lock()
        results[architecture].append((res, cost, energy))
        results.unlock()
    except Exception, e:
        print "% Simulation of " + str(architecture) + " failed due to exception " + str(e)
        traceback.print_exc()
        raise e

def dosim(program, architecture, results, elaborate=0, noise=0.0, postscript=None, lxt=None):
    results.lock()
    if not architecture in results:
      results[architecture] = []
    results.unlock()

    try:
      if elaborate:
        raise KeyError
        
      if noise:
        raise KeyError
        
      if postscript or lxt:
        raise KeyError
    
      (res, energy) = simulate.cache(program, architecture.Generate())
      cost = simulate.cost(architecture.Generate())

      # Avoid absurd architectures
      if cost > 1000:
        if elaborate:
          res = (99999, [])
        else:
          res = 99999
        energy = 99999
        cost = 99999

      #print "% Using cached simulation of " + str(architecture)
      results.lock()
      results[architecture].append((res, cost, energy))
      results.unlock()
    except KeyError:
      address = jobfarm.reserve()
      thread.start_new_thread(_dosim, (program, architecture, address, results, elaborate, noise, postscript, lxt))

def BuildDescription(system):
  m = MapDesc(system.mapper.maxtime, system.mapper.avgtime, system.mapper.avgsqtime, system.mapper.avgenergy, system.mapper.avgresources)
  components = []
  for c in system.architecture.components:
    components.append(ComponentDesc(c.type, c.freq, c.mem, c.bandwidth))
  a = ArchDesc(system.architecture.type, system.architecture.bandwidth, components)
  return SystemDesc(a, m)
